import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import cv2
import numpy as np
from PIL import Image, ImageTk
import os
import functools

class FaceEyeDetectionApp:
    def __init__(self, root):
        self.root = root
        self.root.title("人脸和眼睛检测应用")
        self.root.geometry("1000x700")
        
        # 初始化变量
        self.image_path = None
        self.original_image = None
        self.processed_image = None
        self.detection_method = tk.StringVar(value="haar")
        self.detected_faces = []  # 存储检测到的人脸图像
        self.face_image_views = []  # 存储人脸图像视图
        
        # 鼠标选择相关变量
        self.selecting_face = False
        self.start_x = None
        self.start_y = None
        self.rect = None
        
        self.setup_ui()
        self.load_cascades()
        
    def setup_ui(self):
        # 创建主框架
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 创建控制面板
        control_frame = ttk.LabelFrame(main_frame, text="控制面板")
        control_frame.pack(fill=tk.X, padx=5, pady=5)
        
        # 打开图片按钮
        open_button = ttk.Button(control_frame, text="打开图片", command=self.open_image)
        open_button.pack(side=tk.LEFT, padx=5, pady=5)
        
        # 算法选择
        ttk.Label(control_frame, text="检测算法:").pack(side=tk.LEFT, padx=5, pady=5)
        method_combo = ttk.Combobox(control_frame, textvariable=self.detection_method, 
                                   values=["haar"], state="readonly", width=10)
        method_combo.pack(side=tk.LEFT, padx=5, pady=5)
        method_combo.set("haar")
        ttk.Label(control_frame, text="(目前仅支持Haar级联分类器)").pack(side=tk.LEFT, padx=5, pady=5)
        
        # 检测按钮
        detect_button = ttk.Button(control_frame, text="检测人脸和眼睛", command=self.detect_faces_eyes)
        detect_button.pack(side=tk.LEFT, padx=5, pady=5)
        
        # 选择人脸按钮
        select_face_button = ttk.Button(control_frame, text="选择人脸区域", command=self.toggle_select_face)
        select_face_button.pack(side=tk.LEFT, padx=5, pady=5)
        
        # 保存所有按钮
        save_all_button = ttk.Button(control_frame, text="保存所有结果", command=self.save_all_faces)
        save_all_button.pack(side=tk.LEFT, padx=5, pady=5)
        
        # 创建主显示区域 (左右布局)
        content_frame = ttk.Frame(main_frame)
        content_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # 左侧：图像显示区域
        left_frame = ttk.LabelFrame(content_frame, text="主图像显示")
        left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
        
        # 创建画布用于显示图像
        self.canvas = tk.Canvas(left_frame, bg="lightgray")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        # 绑定鼠标事件用于选择人脸区域
        self.canvas.bind("<Button-1>", self.on_mouse_click)
        self.canvas.bind("<B1-Motion>", self.on_mouse_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_mouse_release)
        
        # 右侧：检测到的人脸列表
        right_frame = ttk.LabelFrame(content_frame, text="检测到的人脸")
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=False, padx=(5, 0))
        right_frame.config(width=250)
        right_frame.pack_propagate(False)  # 固定宽度
        
        # 创建人脸列表的画布和滚动条
        self.faces_canvas = tk.Canvas(right_frame, bg="white")
        faces_scrollbar_y = tk.Scrollbar(right_frame, orient="vertical", command=self.faces_canvas.yview)
        faces_scrollbar_x = tk.Scrollbar(right_frame, orient="horizontal", command=self.faces_canvas.xview)
        
        # 配置画布的滚动命令
        self.faces_canvas.configure(yscrollcommand=faces_scrollbar_y.set, xscrollcommand=faces_scrollbar_x.set)
        
        # 打包组件
        self.faces_canvas.pack(side="left", fill="both", expand=True)
        faces_scrollbar_y.pack(side="right", fill="y")
        faces_scrollbar_x.pack(side="bottom", fill="x")
        
        # 创建可滚动的框架并添加到画布中
        self.faces_scrollable_frame = tk.Frame(self.faces_canvas, bg="white")
        self.faces_canvas_window = self.faces_canvas.create_window((0, 0), window=self.faces_scrollable_frame, anchor="nw")
        
        # 绑定配置事件
        self.faces_scrollable_frame.bind("<Configure>", self.on_frame_configure)
        self.faces_canvas.bind("<Configure>", self.on_canvas_configure)
        # 绑定鼠标滚轮事件
        self.faces_canvas.bind_all("<MouseWheel>", self._on_mousewheel)
        
        # 状态栏
        self.status_var = tk.StringVar(value="就绪")
        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
        status_bar.pack(fill=tk.X, side=tk.BOTTOM, padx=5, pady=5)
        
    def on_frame_configure(self, event):
        """更新滚动区域"""
        self.faces_canvas.configure(scrollregion=self.faces_canvas.bbox("all"))
        
    def on_canvas_configure(self, event):
        """当画布大小改变时更新窗口"""
        self.faces_canvas.itemconfig(self.faces_canvas_window, width=event.width)
        
    def _on_mousewheel(self, event):
        """处理鼠标滚轮事件"""
        self.faces_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
        
    def load_cascades(self):
        # 加载Haar级联分类器
        # 这些是OpenCV自带的预训练模型
        try:
            self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
            self.eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
            
            if self.face_cascade.empty() or self.eye_cascade.empty():
                messagebox.showerror("错误", "无法加载Haar级联分类器，请检查OpenCV安装")
                return False
            return True
        except Exception as e:
            messagebox.showerror("错误", f"加载Haar级联分类器时出错: {str(e)}")
            return False
            
    def open_image(self):
        file_path = filedialog.askopenfilename(
            title="选择图片文件",
            filetypes=[("Image Files", "*.jpg *.jpeg *.png *.bmp *.tiff"), ("All Files", "*.*")]
        )
        
        if file_path:
            self.image_path = file_path
            self.load_image()
            
    def load_image(self):
        try:
            # 使用OpenCV读取图像
            self.original_image = cv2.imread(self.image_path)
            if self.original_image is None:
                messagebox.showerror("错误", "无法加载图片，请确保选择的是有效的图像文件")
                return
                
            self.display_image(self.original_image)
            self.status_var.set(f"已加载图片: {os.path.basename(self.image_path)}")
        except Exception as e:
            messagebox.showerror("错误", f"加载图片时出错: {str(e)}")
            
    def display_image(self, image):
        # 转换颜色空间 (BGR to RGB)
        rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        pil_image = Image.fromarray(rgb_image)
        
        # 获取画布尺寸
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        
        # 如果画布大小为初始值，则使用默认值
        if canvas_width <= 1 or canvas_height <= 1:
            canvas_width = 600
            canvas_height = 400
        
        # 调整图像大小以适应画布，保持纵横比
        img_width, img_height = pil_image.size
        scale = min(canvas_width/img_width, canvas_height/img_height)
        new_width = int(img_width * scale)
        new_height = int(img_height * scale)
        
        resized_image = pil_image.resize((new_width, new_height), Image.LANCZOS)
        self.photo_image = ImageTk.PhotoImage(resized_image)
        
        # 在画布上显示图像
        self.canvas.delete("all")
        x = (canvas_width - new_width) // 2
        y = (canvas_height - new_height) // 2
        self.canvas.create_image(x, y, anchor=tk.NW, image=self.photo_image)
        
        # 保存图像显示的相关信息，用于坐标转换
        self.image_display_info = {
            'x': x,
            'y': y,
            'width': new_width,
            'height': new_height,
            'scale': scale
        }
        
    def toggle_select_face(self):
        """切换选择人脸模式"""
        self.selecting_face = not self.selecting_face
        if self.selecting_face:
            self.status_var.set("请在图像上拖拽选择人脸区域")
        else:
            self.status_var.set("已取消选择模式")
            
    def on_mouse_click(self, event):
        """鼠标点击事件处理"""
        if not self.selecting_face or self.original_image is None:
            return
            
        self.start_x = event.x
        self.start_y = event.y
        
        # 删除之前的矩形框
        if self.rect:
            self.canvas.delete(self.rect)
            
    def on_mouse_drag(self, event):
        """鼠标拖拽事件处理"""
        if not self.selecting_face or self.original_image is None:
            return
            
        # 删除之前的矩形框
        if self.rect:
            self.canvas.delete(self.rect)
            
        # 绘制新的矩形框
        if self.start_x is not None and self.start_y is not None:
            self.rect = self.canvas.create_rectangle(
                self.start_x, self.start_y, event.x, event.y,
                outline="red", width=2
            )
            
    def on_mouse_release(self, event):
        """鼠标释放事件处理"""
        if not self.selecting_face or self.original_image is None:
            return
            
        if self.start_x is not None and self.start_y is not None:
            end_x = event.x
            end_y = event.y
            
            # 确保坐标顺序正确
            x1, x2 = sorted([self.start_x, end_x])
            y1, y2 = sorted([self.start_y, end_y])
            
            # 检查选择区域是否有效
            if abs(x2 - x1) > 10 and abs(y2 - y1) > 10:  # 最小选择区域
                # 转换显示坐标到原始图像坐标
                display_info = self.image_display_info
                orig_x1 = int((x1 - display_info['x']) / display_info['scale'])
                orig_y1 = int((y1 - display_info['y']) / display_info['scale'])
                orig_x2 = int((x2 - display_info['x']) / display_info['scale'])
                orig_y2 = int((y2 - display_info['y']) / display_info['scale'])
                
                # 确保坐标在图像范围内
                orig_x1 = max(0, orig_x1)
                orig_y1 = max(0, orig_y1)
                orig_x2 = min(self.original_image.shape[1], orig_x2)
                orig_y2 = min(self.original_image.shape[0], orig_y2)
                
                # 截取选择区域
                if orig_x2 > orig_x1 and orig_y2 > orig_y1:
                    selected_face = self.original_image[orig_y1:orig_y2, orig_x1:orig_x2]
                    self.detected_faces.append(selected_face)
                    self.display_single_face(len(self.detected_faces)-1, selected_face)
                    self.status_var.set(f"已添加选择的人脸区域，共 {len(self.detected_faces)} 张人脸")
                else:
                    self.status_var.set("选择区域无效，请重新选择")
            else:
                self.status_var.set("选择区域太小，请重新选择")
                
        # 重置选择状态
        self.start_x = None
        self.start_y = None
        self.selecting_face = False
        
    def detect_faces_eyes(self):
        if self.original_image is None:
            messagebox.showwarning("警告", "请先打开一张图片")
            return
            
        try:
            self.status_var.set("正在检测人脸和眼睛...")
            self.root.update_idletasks()
            
            # 清空之前检测到的人脸
            self.clear_detected_faces()
            
            # 复制原始图像用于处理
            img = self.original_image.copy()
            
            # 根据选择的算法进行检测
            method = self.detection_method.get()
            
            if method == "haar":
                # 转换为灰度图
                gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
                
                # 检测人脸
                faces = self.face_cascade.detectMultiScale(
                    gray,
                    scaleFactor=1.1,
                    minNeighbors=5,
                    minSize=(30, 30)
                )
                
                # 在图像上绘制人脸矩形框
                for (x, y, w, h) in faces:
                    cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2)
                    cv2.putText(img, 'Face', (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 0, 0), 2)
                    
                    # 在人脸区域内检测眼睛
                    roi_gray = gray[y:y+h, x:x+w]
                    roi_color = img[y:y+h, x:x+w]
                    
                    eyes = self.eye_cascade.detectMultiScale(roi_gray)
                    for (ex, ey, ew, eh) in eyes:
                        cv2.rectangle(roi_color, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 2)
                        cv2.putText(roi_color, 'Eye', (ex, ey-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
                    
                    # 截取人脸图像
                    face_img = self.original_image[y:y+h, x:x+w]
                    self.detected_faces.append(face_img)
                
                self.processed_image = img
                self.display_image(self.processed_image)
                self.display_detected_faces()
                self.status_var.set(f"检测完成，发现 {len(faces)} 张人脸")
                
            else:
                messagebox.showwarning("警告", f"不支持的检测算法: {method}")
                
        except Exception as e:
            messagebox.showerror("错误", f"检测过程中出错: {str(e)}")
            self.status_var.set("检测失败")
            
    def display_detected_faces(self):
        # 显示检测到的人脸缩略图
        for i, face_img in enumerate(self.detected_faces):
            self.display_single_face(i, face_img)
            
    def display_single_face(self, index, face_img):
        # 创建人脸图像视图框架
        face_frame = tk.LabelFrame(self.faces_scrollable_frame, text=f"人脸 {index+1}", bg="white")
        face_frame.pack(fill=tk.X, padx=5, pady=5)
        
        # 转换为PIL图像
        rgb_face = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB)
        pil_face = Image.fromarray(rgb_face)
        
        # 调整大小
        pil_face.thumbnail((150, 150))
        photo_face = ImageTk.PhotoImage(pil_face)
        
        # 显示图像
        face_label = tk.Label(face_frame, image=photo_face, bg="white")
        face_label.image = photo_face  # 保持引用
        face_label.pack(pady=5)
        
        # 创建按钮框架
        button_frame = tk.Frame(face_frame, bg="white")
        button_frame.pack(fill=tk.X, pady=5)
        
        # 保存按钮
        save_button = tk.Button(button_frame, text="保存", 
                               command=functools.partial(self.save_single_face, index))
        save_button.pack(side=tk.LEFT, padx=5)
        
        # 匹配按钮（替换原来的用作模板按钮）
        match_button = tk.Button(button_frame, text="匹配", 
                                command=functools.partial(self.match_single_face, index))
        match_button.pack(side=tk.LEFT, padx=5)
        
        # 删除按钮，使用functools.partial来正确绑定索引
        delete_button = tk.Button(button_frame, text="删除", 
                                 command=functools.partial(self.delete_single_face, index))
        delete_button.pack(side=tk.RIGHT, padx=5)
        
        # 保存引用
        self.face_image_views.append({
            'frame': face_frame,
            'label': face_label,
            'image': photo_face
        })
            
        # 更新滚动区域
        self.faces_canvas.configure(scrollregion=self.faces_canvas.bbox("all"))
        
    def match_single_face(self, index):
        """使用指定索引的人脸作为模板进行匹配"""
        if self.original_image is None:
            messagebox.showwarning("警告", "请先打开一张图片")
            return
            
        if not (0 <= index < len(self.detected_faces)):
            messagebox.showwarning("警告", "无效的人脸索引")
            return
            
        try:
            self.status_var.set(f"正在使用人脸 {index+1} 进行匹配...")
            self.root.update_idletasks()
            
            # 获取模板人脸
            template = self.detected_faces[index]
            
            # 转换为灰度图
            gray = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2GRAY)
            template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
            
            # 复制图像用于绘制结果
            result_img = self.original_image.copy()
            
            # 模板匹配
            result = cv2.matchTemplate(gray, template_gray, cv2.TM_CCOEFF_NORMED)
            threshold = 0.6  # 匹配阈值
            locations = np.where(result >= threshold)
            
            # 获取模板尺寸
            template_h, template_w = template_gray.shape
            
            match_count = 0
            # 绘制匹配结果
            for pt in zip(*locations[::-1]):
                # 避免在原始模板位置绘制匹配框
                skip = False
                for face in self.get_all_face_locations():
                    if (abs(pt[0] - face[0]) < template_w/2 and 
                        abs(pt[1] - face[1]) < template_h/2):
                        skip = True
                        break
                
                if not skip:
                    cv2.rectangle(result_img, pt, (pt[0] + template_w, pt[1] + template_h), (0, 255, 0), 2)
                    cv2.putText(result_img, f'Face {index+1}', (pt[0], pt[1]-10), 
                               cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1)
                    match_count += 1
                    
            # 显示匹配结果
            self.processed_image = result_img
            self.display_image(self.processed_image)
            self.status_var.set(f"匹配完成，找到 {match_count} 个匹配项")
            
        except Exception as e:
            messagebox.showerror("错误", f"匹配过程中出错: {str(e)}")
            self.status_var.set("匹配失败")
            
    def get_all_face_locations(self):
        """获取所有人脸的位置信息"""
        locations = []
        if self.original_image is not None:
            # 这里简化处理，实际应用中可以存储人脸位置信息
            gray = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2GRAY)
            faces = self.face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
            for (x, y, w, h) in faces:
                locations.append((x, y))
        return locations
            
    def clear_detected_faces(self):
        # 清空检测到的人脸列表
        self.detected_faces.clear()
        
        # 销毁所有的人脸视图
        for view in self.face_image_views:
            view['frame'].destroy()
        self.face_image_views.clear()
        
        # 更新滚动区域
        self.faces_canvas.configure(scrollregion=self.faces_canvas.bbox("all"))
            
    def delete_single_face(self, index):
        # 删除指定索引的人脸
        if 0 <= index < len(self.detected_faces):
            # 从列表中删除人脸
            self.detected_faces.pop(index)
            
            # 销毁对应的视图组件
            self.face_image_views[index]['frame'].destroy()
            
            # 从视图列表中删除对应项
            self.face_image_views.pop(index)
            
            # 更新剩余视图的编号
            self.update_face_labels()
            
            self.status_var.set(f"已删除人脸，剩余 {len(self.detected_faces)} 张人脸")
    
    def update_face_labels(self):
        # 更新剩余人脸的编号标签
        for i in range(len(self.face_image_views)):
            frame = self.face_image_views[i]['frame']
            # 更新框架的文本标签
            try:
                frame.config(text=f"人脸 {i+1}")
            except tk.TclError:
                # 如果组件已被销毁，跳过
                pass
    
    def save_single_face(self, index):
        if 0 <= index < len(self.detected_faces):
            face_img = self.detected_faces[index]
            file_path = filedialog.asksaveasfilename(
                defaultextension=".jpg",
                filetypes=[("JPEG", "*.jpg"), ("PNG", "*.png"), ("BMP", "*.bmp"), ("All Files", "*.*")],
                title=f"保存人脸 {index+1}"
            )
            
            if file_path:
                try:
                    cv2.imwrite(file_path, face_img)
                    self.status_var.set(f"人脸 {index+1} 已保存到: {file_path}")
                    messagebox.showinfo("成功", f"人脸 {index+1} 保存成功")
                except Exception as e:
                    messagebox.showerror("错误", f"保存人脸时出错: {str(e)}")
    
    def save_all_faces(self):
        if not self.detected_faces:
            messagebox.showwarning("警告", "没有检测到的人脸可保存")
            return
            
        # 选择保存目录
        directory = filedialog.askdirectory(title="选择保存目录")
        if directory:
            try:
                saved_count = 0
                for i, face_img in enumerate(self.detected_faces):
                    file_path = os.path.join(directory, f"face_{i+1}.jpg")
                    cv2.imwrite(file_path, face_img)
                    saved_count += 1
                    
                self.status_var.set(f"已保存 {saved_count} 张人脸到: {directory}")
                messagebox.showinfo("成功", f"已保存 {saved_count} 张人脸")
            except Exception as e:
                messagebox.showerror("错误", f"保存人脸时出错: {str(e)}")
            
    def save_image(self):
        if self.processed_image is None:
            messagebox.showwarning("警告", "没有检测结果可保存，请先进行检测")
            return
            
        file_path = filedialog.asksaveasfilename(
            defaultextension=".jpg",
            filetypes=[("JPEG", "*.jpg"), ("PNG", "*.png"), ("BMP", "*.bmp"), ("All Files", "*.*")],
            title="保存检测结果"
        )
        
        if file_path:
            try:
                cv2.imwrite(file_path, self.processed_image)
                self.status_var.set(f"图像已保存到: {file_path}")
                messagebox.showinfo("成功", "图像保存成功")
            except Exception as e:
                messagebox.showerror("错误", f"保存图像时出错: {str(e)}")

def main():
    root = tk.Tk()
    app = FaceEyeDetectionApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()